<!DOCTYPE html>
<!--
Copyright 2017 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/core/test_utils.html">
<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
<link rel="import" href="/tracing/metrics/media_metric.html">
<link rel="import" href="/tracing/value/histogram_set.html">

<script>
'use strict';

tr.b.unittest.testSuite(function() {
  // Arbitrarily selected process ID and thread IDs we'll use in test data
  const procId = 52;
  const tidMain = 1;
  const tidCompositor = 53;
  const tidAudio = 55;

  function doLoadEvent(timestamp, opt_id) {
    return {name: 'WebMediaPlayerImpl::DoLoad', args: {id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
  }

  function videoRenderEvent(timestamp, opt_id) {
    return {name: 'VideoRendererImpl::Render', args: {id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'};
  }

  function audioRenderEvent(timestamp, opt_id) {
    return {name: 'AudioRendererImpl::Render', args: {id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidAudio, ph: 'X'};
  }

  function videoFramesDroppedEvent(timestamp, frameCount, opt_id) {
    return {name: 'VideoFramesDropped',
      args: {count: frameCount, id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidCompositor, ph: 'X'};
  }

  function onEndedEvent(timestamp, mediaDuration, opt_id) {
    return {name: 'WebMediaPlayerImpl::OnEnded',
      args: {duration: mediaDuration, id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
  }

  function doSeekEvent(timestamp, targetTime, opt_id) {
    return {name: 'WebMediaPlayerImpl::DoSeek',
      args: {target: targetTime, id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
  }

  function seekedEvent(timestamp, targetTime, opt_id) {
    return {name: 'WebMediaPlayerImpl::OnPipelineSeeked',
      args: {target: targetTime, id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
  }

  function bufferEnoughEvent(timestamp, opt_id) {
    return {name: 'WebMediaPlayerImpl::BufferingHaveEnough',
      args: {id: opt_id || 0},
      pid: procId, ts: timestamp, cat: 'media', tid: tidMain, ph: 'X'};
  }

  function threadMarker(threadName, threadId) {
    return {name: 'thread_name', args: {name: threadName},
      pid: procId, ts: 0, cat: '__metadata', tid: threadId, ph: 'M'};
  }

  const mainThreadMarker = threadMarker('CrRendererMain', tidMain);
  const compositorThreadMarker = threadMarker('Compositor', tidCompositor);
  const audioThreadMarker = threadMarker('AudioOutputDevice', tidAudio);

  function makeModel(events) {
    return tr.c.TestUtils.newModelWithEvents([events]);
  }

  function checkCloseTo(histograms, histogramName, expectedValue) {
    assert.isDefined(histograms.getHistogramNamed(histogramName));
    const value = histograms.getHistogramNamed(histogramName);
    const statistics = value.running;
    assert.strictEqual(statistics.count, 1);
    assert.closeTo(statistics.mean, expectedValue, 1e-5);
  }

  function checkCloseToMultiple(histograms, histogramName, expectedCount,
      expectedMin, expectedMean, expectedMax) {
    assert.isDefined(histograms.getHistogramNamed(histogramName));
    const value = histograms.getHistogramNamed(histogramName);
    const statistics = value.running;
    assert.strictEqual(statistics.count, expectedCount);
    assert.closeTo(statistics.min, expectedMin, 1e-5);
    assert.closeTo(statistics.mean, expectedMean, 1e-5);
    assert.closeTo(statistics.max, expectedMax, 1e-5);
  }

  function checkEqual(histograms, histogramName, expectedValue) {
    assert.isDefined(histograms.getHistogramNamed(histogramName));
    const value = histograms.getHistogramNamed(histogramName);
    const statistics = value.running;
    assert.strictEqual(statistics.count, 1);
    assert.strictEqual(statistics.mean, expectedValue);
  }

  test('mediaMetric_noData', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.lengthOf(histograms, 0);
  });

  test('mediaMetric_videoTimeToPlay', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(100),
      videoRenderEvent(300),
      // Video renderer always generate multiple render events,
      // one for each frame. For calculation of time-to-play,
      // only the first render event is relevant. Here we put in
      // a second render event to make sure it's ignored by the
      // metric computation code.
      videoRenderEvent(400),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'time_to_video_play', 0.2);
  });

  test('mediaMetric_audioTimeToPlay', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      mainThreadMarker,
      audioThreadMarker,
      doLoadEvent(1000),
      audioRenderEvent(1100),
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'time_to_audio_play', 0.1);
  });

  test('mediaMetric_bufferingTimeVideo', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      videoRenderEvent(1600),
      onEndedEvent(10051500, 10),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'buffering_time', 50);
  });

  test('mediaMetric_bufferingTimeAudio', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      mainThreadMarker,
      audioThreadMarker,
      doLoadEvent(1000),
      audioRenderEvent(1500),
      onEndedEvent(5002500, 5),
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'buffering_time', 1);
  });

  // With seek, no buffering time should be reported
  test('mediaMetric_noBufferingTime', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      videoRenderEvent(1600),
      onEndedEvent(10066666, 10),
      doSeekEvent(525, 1.2),
      seekedEvent(719, 1.2),
      bufferEnoughEvent(825),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isUndefined(histograms.getHistogramNamed('buffering_time'));
  });

  test('mediaMetric_droppedFrameCount', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      videoFramesDroppedEvent(123456, 3),
      videoFramesDroppedEvent(234567, 6),
      videoFramesDroppedEvent(345678, 1),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkEqual(histograms, 'dropped_frame_count', 10);
  });

  test('mediaMetric_droppedFrameCountZero', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkEqual(histograms, 'dropped_frame_count', 0);
  });

  test('mediaMetric_droppedFrameCountNoVideo', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      audioRenderEvent(1500),
      mainThreadMarker,
      audioThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count'));
  });

  test('mediaMetric_seekTimeVideo', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      doSeekEvent(2000, 1.2),
      seekedEvent(2500, 1.2),
      bufferEnoughEvent(3000),
      doSeekEvent(15000, 3.7),
      seekedEvent(75000, 3.7),
      bufferEnoughEvent(95000),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5);
    checkCloseTo(histograms, 'seek_time_1_2', 1);
    checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60);
    checkCloseTo(histograms, 'seek_time_3_7', 80);
  });

  test('mediaMetric_seekTimeAudio', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      audioRenderEvent(1500),
      doSeekEvent(2000, 1.2),
      seekedEvent(2500, 1.2),
      bufferEnoughEvent(3000),
      doSeekEvent(15000, 3.7),
      seekedEvent(75000, 3.7),
      bufferEnoughEvent(95000),
      mainThreadMarker,
      audioThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'pipeline_seek_time_1_2', 0.5);
    checkCloseTo(histograms, 'seek_time_1_2', 1);
    checkCloseTo(histograms, 'pipeline_seek_time_3_7', 60);
    checkCloseTo(histograms, 'seek_time_3_7', 80);
  });

  test('mediaMetric_seekOverlap', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      doSeekEvent(2000, 1.2),
      seekedEvent(2500, 1.2),
      doSeekEvent(2800, 3.7), // Out of order
      bufferEnoughEvent(3000),
      seekedEvent(75000, 3.7),
      bufferEnoughEvent(95000),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
    assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
  });

  test('mediaMetric_seekIncomplete', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      doSeekEvent(2000, 1.2),
      seekedEvent(2500, 1.2),
      // Missing bufferEnoughEvent
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
    assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
  });

  test('mediaMetric_seekWrongTarget', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(1500),
      doSeekEvent(2000, 1.2),
      seekedEvent(2500, 2.7), // Wrong target, should be 1.2
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isDefined(histograms.getHistogramNamed('time_to_video_play'));
    assert.isUndefined(histograms.getHistogramNamed('seek_time_1_2'));
  });

  test('mediaMetric_multiplePlaybacks', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000, 0),
      doLoadEvent(1100, 1),
      videoRenderEvent(1400, 1),
      videoRenderEvent(1500, 0),
      doLoadEvent(2000, 2),
      doSeekEvent(2000, 1.2, 0),
      doSeekEvent(2100, 1.2, 1),
      videoRenderEvent(2100, 2),
      doSeekEvent(2200, 1.2, 2),
      seekedEvent(2500, 1.2, 2),
      seekedEvent(2600, 1.2, 1),
      seekedEvent(2700, 1.2, 0),
      bufferEnoughEvent(3000, 0),
      bufferEnoughEvent(3100, 1),
      bufferEnoughEvent(3800, 2),
      mainThreadMarker,
      compositorThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    // Values are 0.5, 0.3, 0.1, with min 0.1, mean 0.3, max 0.5.
    checkCloseToMultiple(histograms, 'time_to_video_play', 3, 0.1, 0.3, 0.5);
    // Values are 0.7, 0.5, 0.3, with min 0.3, mean 0.5, max 0.7.
    checkCloseToMultiple(histograms,
        'pipeline_seek_time_1_2', 3, 0.3, 0.5, 0.7);
    // Values are 1, 1, 1.6, with min 1, mean 1.2, max 1.6
    checkCloseToMultiple(histograms, 'seek_time_1_2', 3, 1, 1.2, 1.6);
  });

  // Scenario: Play mixed audio/video from start to finish
  test('mediaMetric_playVideoScenario', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(2000),
      videoRenderEvent(3000),
      audioRenderEvent(3200),
      videoRenderEvent(3300),
      videoFramesDroppedEvent(123456, 4),
      videoFramesDroppedEvent(234567, 2),
      onEndedEvent(10013000, 10),
      mainThreadMarker,
      compositorThreadMarker,
      audioThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'time_to_video_play', 1);
    checkCloseTo(histograms, 'time_to_audio_play', 1.2);
    checkCloseTo(histograms, 'buffering_time', 10);
    checkEqual(histograms, 'dropped_frame_count', 6);
  });

  // Scenario: Play audio from start to finish
  test('mediaMetric_playAudioScenario', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      audioRenderEvent(1500),
      onEndedEvent(10002500, 10),
      mainThreadMarker,
      audioThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    assert.isUndefined(histograms.getHistogramNamed('time_to_video_play'));
    checkCloseTo(histograms, 'time_to_audio_play', 0.5);
    checkCloseTo(histograms, 'buffering_time', 1);
    assert.isUndefined(histograms.getHistogramNamed('dropped_frame_count'));
  });

  // Scenario: Play audio/video with two seeks
  test('mediaMetric_seekScenario', function() {
    const histograms = new tr.v.HistogramSet();
    const events = [
      doLoadEvent(1000),
      videoRenderEvent(2000),
      audioRenderEvent(2020),
      videoRenderEvent(2040),
      doSeekEvent(5000, 0.5),
      seekedEvent(5200, 0.5),
      bufferEnoughEvent(6000),
      videoFramesDroppedEvent(123456, 4),
      doSeekEvent(200000, 9),
      seekedEvent(210000, 9),
      bufferEnoughEvent(220000),
      videoFramesDroppedEvent(234567, 2),
      onEndedEvent(300000, 10),
      mainThreadMarker,
      compositorThreadMarker,
      audioThreadMarker,
    ];
    tr.metrics.mediaMetric(histograms, makeModel(events));
    checkCloseTo(histograms, 'time_to_video_play', 1);
    checkCloseTo(histograms, 'time_to_audio_play', 1.02);
    assert.isUndefined(histograms.getHistogramNamed('buffering_time'));
    checkEqual(histograms, 'dropped_frame_count', 6);
    checkCloseTo(histograms, 'pipeline_seek_time_0_5', 0.2);
    checkCloseTo(histograms, 'seek_time_0_5', 1);
    checkCloseTo(histograms, 'pipeline_seek_time_9', 10);
    checkCloseTo(histograms, 'seek_time_9', 20);
  });
});
</script>
